fix(metadata-protocol): an org-scoped DELETE no longer evicts the env-wide registry entry (#6780) - #6826
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 8, 2026 23:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6780
The defect: one tenant's "reset" evicted the registry entry every tenant shares
restoreArtifactRegistryView— the three-tier heal that repairs the in-memorySchemaRegistryafter an overlay-row delete — is(type, name)-addressed and org-blind. All three tiers write the plain key:removeRuntimeShadowdrops it, the layer-2 baseline re-register rewrites it,removeOverlayEntryretires it. Per ADR-0005 there is exactly one plain-key entry per(type, name)in a process, and it belongs to the env-wide row — an org-scoped overlay never enters the registry at all since #6602.deleteMetaItemcalled the heal on all three of its call sites without passing the delete's own scope (const orgId = request.organizationId ?? nullfed the ROW delete only), so org A resetting its own customization reached in and cleared the entry every other org and the control plane read.Premise re-measured on today's
origin/main(5e247fd) — it holdsOn an unscoped control-plane kernel — the shape #5086 measured the flagship showcase actually booting with:
The env-wide row still sits in
sys_metadata; only its registry entry is gone. While it is gone, direct registry readers answer as if the item does not exist — ADR-0110 D3's declaration gate,resolveRouteActionDeclaration, fail-closedassertObjectRegistered(404). One tenant's "reset my customization" degrades every other tenant's runtime until restart.The self-heal branch is the cheaper door still: when org A has no overlay row at all, the delete receipt is
{reset: false, "nothing to delete"}— and the heal ran anyway, evicting the entry just the same. A gate on the delete-ful branch alone would have left that door open.The fix: the verdict moves inside the helper —
organizationIdas a required parameter (shape b)The card offered two precedent shapes. (b) was chosen over (a) for a measured reason: there are four call sites, not the two the card names —
deleteMetaItemhas three (self-heal, post-repo.delete, legacy raw-engine path) andrevertCommitone. PR #6807 had already gated the revert one; a call-site fix would have had to find the other three, and the legacy raw-engine path is exactly the kind a sweep misses. A required parameter makes the next caller answer at compile time; an optional one would default an omission back to "env-wide" and reinstate the hole. Same shape as #6602 / PR #6779'shydrateOverlayIntoRegistryon the register side.#6807's call-site
if (orgId === null)is now redundant-not-contradictory and was folded into the argument it passes — its pin still covers the batch path, and it goes red if the gate is ever removed (see below).Register wide, retire narrow. The write-through's
objectcarve-out stays un-org-gated and deliberately does NOT transfer to the delete side: it rests onassertObjectRegisteredfailing CLOSED, so a surplus entry degrades to "listable but rowless" and the next reload heals it, while a wrongly retired entry 404s data CRUD for every tenant. The two costs are asymmetric, so the two gates are too.Unchanged: row-level delete behaviour (an org delete still removes the org row; that org's next read falls back to the env-wide body); the env-wide delete's full three-tier walk (#6687 tier 1 un-shadow, #5079 tier 3 retirement); and the kernel scope gate, which still guards re-registration only, because that is a fact about the kernel, not about this row.
Reverse verification (direction predicted before running)
With the
organizationIdrefusal branch removed fromrestoreArtifactRegistryView, predicted 3 red / 4 green in the new block; measured 3 red / 4 green:The first red is precisely the
undefinedthe card measured, reproduced on demand. The four greens are not slack: the org-scoped ROW control asserts the delete itself and never reads the registry (a fake fix that skipped the delete entirely would fail there), and the three env-wide cases are the reason this heal exists — #6687's three-tier walk must not regress.A fourth red lands in a file this PR does not edit, and it is the point of shape (b) rather than a surprise: PR #6807's own pin (
protocol-commit-history.test.ts→ "an ORG-scoped soft-remove leaves the env-wide registry entry alone") goes red too —expected null to be 'EnvWide'— because its call-site gate was folded into the argument it now passes. The gate moved; the coverage did not.Tests
Extended in #6602's existing pin file (
protocol-org-overlay-registry-gate.test.ts) rather than a parallel new one. That file's pre-existing#6602 — the delete chain needs no re-keying under this fixblock also got an honest comment correction: its conclusion holds for a name only one org has touched, and fails when the same name also has an env-wide row —(type, name)cannot tell the two apart, so "no entry of its own" was never "no entry". This PR keeps the file's conclusion (no org-scoped registry keys) and adds the missing half (an org-scoped delete must not heal at all).All 32 family gates inside
.github/workflows/lint.yml's ESLint job were run individually in the foreground — every one PASS (incl.check:engine-double-contract,check:route-envelope,check:error-code-casing,check:meta-type-normalized,check:nul-bytes,check:adr-anchors).Generated by Claude Code